package han.chensing.latos.common.level.util;

import han.chensing.latos.common.level.content.timeLine.TimeLineEventSet;
import han.chensing.latos.common.level.content.timeLine.events.TimeLineEvent;
import han.chensing.latos.common.level.content.timeLine.frame.FrameEditor;

import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

public class BeatUtil {
    Supplier<TimeLineEventSet<TimeLineEvent<FrameEditor>, FrameEditor>> bpmSet;
    public BeatUtil(Supplier<TimeLineEventSet<TimeLineEvent<FrameEditor>,FrameEditor>> bpmSet) {
        this.bpmSet = bpmSet;
    }

    public void queryBeatsForward(float time,BeatQueryFunction beatQueryFunction){
        queryBeats(time,beatQueryFunction,this::getNextBeatTime);
    }

    public void queryBeatsBackward(float time,BeatQueryFunction beatQueryFunction){
        queryBeats(time,beatQueryFunction,this::getLastBeatTime);
    }

    public void queryBeats(float time, BeatQueryFunction beatQueryFunction, Function<Float,Float> timeFunction){
       boolean go;
       float lastTime=-Float.MAX_VALUE;
       do {
           FrameEditor frame = getNearestBackBpmChangeFrame(time);
           if (frame==null)break;
           float nowBpm = frame.getBpm();
           time= timeFunction.apply(time);
           int baseBeat = getBaseBeat(time);
           go=beatQueryFunction.query(time,nowBpm,baseBeat);
           if (lastTime==time)break;
           lastTime=time;
       }while (go);
    }

    public float getNearBackBeatTime(float time){
        int baseBeat = getBaseBeat(time);
        float nearestBackBpmChangeTime = getNearestBackBpmChangeTime(time);
        float estimatedNearLastBeatTime = beatToSecond(time, baseBeat);
        return Math.max(estimatedNearLastBeatTime, nearestBackBpmChangeTime);
    }

    public float getNearFrontBeatTime(float time){
        int baseBeat = getBaseBeat(time)+1;
        float nearestBackBpmChangeTime = getNearestFrontBpmChangeTime(time);
        float estimatedNearNextBeatTime = beatToSecond(time, baseBeat);
        return Math.min(estimatedNearNextBeatTime, nearestBackBpmChangeTime);
    }

    public float getLastBeatTime(float time){
        float estimatedLastBeat = getBaseBeat(time) -1.0001f;
        float estimatedLastTime = beatToSecond(time, estimatedLastBeat);
        float nearestBackBpmChangeTime = getNearestBackBpmChangeTime(time);
        return Math.max(estimatedLastTime,nearestBackBpmChangeTime);
    }

    public float getNextBeatTime(float time){
        float estimatedNextBeat = getBaseBeat(time) + 1.0001f;
        float estimatedNextTime = beatToSecond(time, estimatedNextBeat);
        float nearestFrontBpmChangeTime = getNearestFrontBpmChangeTime(time);
        return Math.min(estimatedNextTime,nearestFrontBpmChangeTime);
    }

    public int getBaseBeat(float time){
        float bpm = getNowBpm(time);
        float backBpmChangeTime = getNearestBackBpmChangeTime(time);
        float realTime = time - backBpmChangeTime;
        return (int) Math.floor(secondToMinute(realTime) * bpm);
    }

    public float[] getBaseBeatAndRest(float time){
        float bpm = getNowBpm(time);
        float backBpmChangeTime = getNearestBackBpmChangeTime(time);
        float realTime = time - backBpmChangeTime;
        float beat= secondToMinute(realTime) * bpm;
        float beatFloor=(float) Math.floor(beat);
        float beatRest=beat-beatFloor;
        return new float[]{beatFloor,beatRest};
    }

    public float getNowBpm(float time){
        FrameEditor frame = getNearestBackBpmChangeFrame(time);
        return frame==null?0:frame.getBpm();
    }

    public float getNearestBackBpmChangeTime(float time){
        FrameEditor frame = getNearestBackBpmChangeFrame(time);
        return frame==null?-Float.MAX_VALUE:frame.getTime();
    }

    public FrameEditor getNearestBackBpmChangeFrame(float time){
        List<FrameEditor> frames = checkBpmSet();
        if (frames==null||frames.isEmpty())return null;
        return TimeUtil.getLastHasTime(frames, time);
    }

    public float getNearestFrontBpmChangeTime(float time){
        List<FrameEditor> frames = checkBpmSet();
        if (frames==null||frames.isEmpty())return Float.MAX_VALUE;
        FrameEditor nextHasTime = TimeUtil.getNextHasTime(frames, time);
        return nextHasTime==null?Float.MAX_VALUE:nextHasTime.getTime();
    }

    private float secondToMinute(float second){
        return second/60f;
    }

    private float minuteToSecond(float minute){
        return minute*60f;
    }

    private float beatToSecondDirectly(float time,float beat){
        float bpm = getNowBpm(time);
        return minuteToSecond(beat/bpm);
    }

    private float beatToSecond(float time,float beat){
        float nearestBackBpmChangeTime = getNearestBackBpmChangeTime(time);
        return nearestBackBpmChangeTime+beatToSecondDirectly(time, beat);
    }

    private List<FrameEditor> checkBpmSet(){
        if (ObjectUtil.checkNull(bpmSet,bpmSet.get()==null))return null;
        return bpmSet.get().getKeyFrames();
    }

    public interface BeatQueryFunction{
        boolean query(float time,float nowBpm,int relativeBeat);
    }
}
